package main

import (
	"bufio"
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/signal"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"unsafe"

	"github.com/hajimehoshi/oto"
)

/*
#cgo LDFLAGS: -Wl,-rpath=/usr/local/lib/xftts -L/usr/local/lib/xftts -lxtts
#include <stdlib.h>
extern int tts_init();
extern int tts_init2(void *params);
extern int tts_init3(void *params,int voice);
extern void tts_close();
extern int tts_puts(const char *src_text);
extern void* tts_get_pcm(int *audio_len,int *err); //PCM,单通道，采样率16000,每秒字节数32000
*/
import "C"

var skipN int32
var curPos int32 = 0
var playPosLog int32

var fullName string
var bakFullName string

const lineLimit int = 60

func myUsage() {
	fmt.Println("格式：xfttsgo [选项] [文件或目录]")
}

var stdi = flag.Bool("stdin", false, "从标准输入读取文本")

func main() {
	flagContinue := flag.Bool("c", false, "在任意位置继续播放上次的进度。")
	v := flag.Bool("log", false, "查看上次阅读记录。")
	dir := flag.Bool("d", false, "播放目录，此时'-skip'参数无效")
	skip := flag.Int("skip", 0, "跳过段数")
	out := flag.String("o", "", "输出文件，保存为WAV音频文件，此时'-d -skip'参数无效")
	h := flag.Bool("h", false, "显示帮助信息")
	voice := flag.Int("voice", 1, "选择声音。1-女声，2-男声。")

	flag.Parse()

	arg0 := flag.Arg(0)

	if *flagContinue {
		arg0 = getLastDir()
		if arg0 != "" {
			fmt.Printf("播放目录：%s\n", arg0)
		} else {
			fmt.Println("错误：无播放记录。")
			return
		}
		f := true
		dir = &f
	}

	if *h {
		myUsage()
		flag.Usage()
		return
	}

	if *v {
		viewLog()
		return
	}

	if len(*out) > 0 {
		fileToWav(arg0, *out, *voice)
		return
	}

	if *stdi == false && arg0 == "" {
		myUsage()
		flag.Usage()
		return
	}

	skipN = int32(*skip)
	playPosLog = skipN

	var wg sync.WaitGroup
	var audioCh chan PcmMsg = make(chan PcmMsg, 1)
	wg.Add(1)
	go playBack(audioCh, &wg)
	if *stdi {
		fullName = ""
	} else {
		fullName = getFullName(arg0)
		logTxtDir(fullName)
	}

	var txtDir string

	go wait_sig()

	var txtList = []string{}
	var err error
	if *dir == false {
		if *stdi == false {
			txtList = append(txtList, filepath.Base(arg0))
			txtDir = filepath.Dir(fullName)
		} else {
			txtList = append(txtList, "")
			txtDir = filepath.Dir(".")
		}

	} else {
		var n int
		txtDir = fullName
		txtList, n, err = getListWithSkip(txtDir)
		if err != nil {
			panic(err)
		}
		skipN = int32(n)
		atomic.StoreInt32(&playPosLog, skipN)
	}

	initMSC()
	if ttsInit2(*voice) == false {
		if C.tts_init() != 0 {
			panic("tts_init")
		}
	}
	defer C.tts_close()

	for i := range txtList {
		if *stdi {
			bakFullName = ""
		} else {
			bakFullName = filepath.Join(txtDir, txtList[i])
			if i == 0 {
				fullName = bakFullName
			}
			fmt.Println("Play:", txtList[i], skipN)
		}

		speakFile(bakFullName, audioCh)
		if *stdi {
			break
		}
		skipN = 0
	}

	close(audioCh)
	wg.Wait()
	logFileName(-1)
}

func arrayReduce(a []string) []string {
	names := make([]string, 0, len(a))
	for _, name := range a {
		if strings.HasSuffix(strings.ToLower(name), ".txt") {
			names = append(names, name)
		}
	}
	return names
}

func getListWithSkip(dir string) (txtList []string, skip int, err error) {
	d, err := os.Open(dir)
	if err != nil {
		return
	}
	defer d.Close()
	names, err := d.Readdirnames(-1)
	if err != nil {
		return
	}

	names = arrayReduce(names)

	sort.Strings(names)

	logFile, err := os.Open(filepath.Join(dir, "xfttsgo.log"))
	if err != nil {
		txtList = names
		skip = 0
		err = nil
		return
	}
	defer logFile.Close()
	buflog := bufio.NewReader(logFile)
	line1, _, err := buflog.ReadLine()
	if err != nil {
		return
	}
	var logName = filepath.Base(string(line1[5:]))
	line2, _, err := buflog.ReadLine()
	if err != nil {
		return
	}
	skip, err = strconv.Atoi(string(line2[4:]))
	if err != nil {
		return
	}

	var n int
	for n = 0; n < len(names); n++ {
		if names[n] == logName {
			break
		}
	}
	if skip < 0 {
		skip = 0
		n++
	}
	if n >= len(names) {
		n = 0
	}
	txtList = names[n:]
	return
}

func speakFile(fn string, audioCh chan PcmMsg) error {
	var fp io.Reader
	if fn == "" {
		fp = os.Stdin
	} else {
		fp1, err := os.Open(fn)
		if err != nil {
			return err
		}
		defer fp1.Close()
		fp = fp1
	}

	curPos = 0
	reader := bufio.NewReader(fp)

	for {
		line1, err := reader.ReadString('\n')
		if err != nil {
			//log.Println(err)
			break
		}

		if len(line1) == 0 {
			continue
		}

		line1 = strings.TrimSpace(line1)

		runes1 := bytes.Runes([]byte(line1))

		//fmt.Println(string(runes1))

		if len(runes1) < lineLimit {
			if speakText(string(runes1), audioCh) == 1 {
				goto END1
			}
			continue
		}
		for {
			pos := getBreakPos(runes1)
			if len(runes1)-pos < 8 {
				pos = len(runes1)
			}
			if pos > 0 {
				if speakText(string(runes1[0:pos]), audioCh) == 1 {
					goto END1
				}
			}
			if len(runes1) > pos {
				runes1 = runes1[pos:]
			} else {
				break
			}
		}
	}
END1:
	return nil
}

func getLastDir() string {
	home1, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	log1 := filepath.Join(home1, ".xfttsgo", "playdir.log")
	v, err := ioutil.ReadFile(log1)
	if err != nil {
		return ""
	}
	return string(v)
}
func logTxtDir(fn string) {
	info, err := os.Stat(fn)
	if err != nil {
		return
	}
	d := fn
	if info.IsDir() == false {
		d = filepath.Dir(fn)
	}
	home1, err := os.UserHomeDir()
	if err != nil {
		return
	}
	log1 := filepath.Join(home1, ".xfttsgo", "playdir.log")
	fp, err := os.Create(log1)
	if err != nil {
		return
	}
	defer fp.Close()
	fp.Write([]byte(d))
}

func getFullName(fn string) string {
	if strings.HasPrefix(fn, "/") {
		return fn
	}

	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	return filepath.Join(wd, fn)
}

func initMSC() {
	home, err := os.UserHomeDir()
	if err != nil {
		panic(err)
	}
	os.Chdir(filepath.Join(home, ".xfttsgo"))
}

func logFileName(pos int32) {
	home, err := os.UserHomeDir()
	if err != nil {
		panic(err)
	}
	s := fmt.Sprintf("File:%s\nPos:%d\n", filepath.Base(fullName), pos)
	os.MkdirAll(filepath.Join(home, ".xfttsgo"), 777)
	os.Chdir(filepath.Join(home, ".xfttsgo"))
	ioutil.WriteFile("xfttsgo.log", []byte(s), 0644)

	ioutil.WriteFile(filepath.Join(filepath.Dir(fullName), "xfttsgo.log"), []byte(s), 0644)

}

func viewLog() {
	data, err := ioutil.ReadFile("xfttsgo.log")
	if err == nil {
		fmt.Println(string(data))
		return
	}

	home, err := os.UserHomeDir()
	if err != nil {
		panic(err)
	}
	data, err = ioutil.ReadFile(filepath.Join(home, ".xfttsgo", "xfttsgo.log"))
	if err != nil {
		fmt.Println("没有阅读记录。")
	} else {
		fmt.Println(string(data))
	}
}

func getBreakPos(data []rune) (pos int) {
	var ends1 = []byte("。！？?!;；:：,，. 、")
	//const ends2 = `;；:：,，. 、`
	runes1 := bytes.Runes(ends1)
	//runes2 := bytes.Runes([]byte(ends2))
	pos = 0
	//fmt.Println(len(data), max)
	if len(data) <= lineLimit {
		pos = len(data)
		return
	}
	for {
		if pos < lineLimit {
			goto BREAKEND1
		}
		for _, r := range runes1 {
			if r == data[pos] {
				//fmt.Println("return", pos)
				pos++
				return
			}
		}
		// if pos > lineLimit {
		// 	for _, r := range runes2 {
		// 		if r == data[pos] {
		// 			//fmt.Println("return", pos)
		// 			pos++
		// 			return
		// 		}
		// 	}
		// }
	BREAKEND1:
		pos++
		if pos >= len(data) {
			break
		}
	}
	return
}

type PcmMsg struct {
	Text string
	Pcm  []byte
	Pos  int32
}

func speakText(s string, audioCh chan PcmMsg) (ret int) {
	ret = 0
	s = strings.TrimSpace(s)
	if s == "" {
		return
	}
	if atomic.LoadInt32(&userCmd) == 1 {
		atomic.StoreInt32(&userCmd, 0)
		ret = 1
		return
	}
	curPos++
	//fmt.Println("pos:", curPos)
	if curPos <= skipN {
		return
	}
	//fmt.Println(s)
	t := C.CString(s)
	defer C.free(unsafe.Pointer(t))

	if C.tts_puts(t) != 0 {
		fmt.Println("tts_puts error")
		return
	}

	var n C.int
	var errno C.int
	buf := bytes.NewBufferString("")
	for {
		p := C.tts_get_pcm(&n, &errno)
		if errno == -1 {
			break
		}
		if n > 0 && p != nil {
			data := C.GoBytes(p, n)
			buf.Write(data)
		}

	}

	audioCh <- PcmMsg{s, buf.Bytes(), curPos}
	return
}

var userCmd int32 = 0
var statSkip bool = false

func playBack(ch chan PcmMsg, wg *sync.WaitGroup) {
	defer wg.Done()

	var locker sync.Mutex
	if *stdi == false {
		go func() {
			var flag bool = false
			for {
				var s string
				fmt.Scanln(&s)
				switch s {
				case "n":
					atomic.StoreInt32(&userCmd, 1)
					statSkip = true
				default:
					if flag {
						locker.Unlock()
						flag = false
						fmt.Println(">>继续阅读")
					} else {
						locker.Lock()
						flag = true
						fmt.Println(">>阅读暂停")
					}
				}
			}
		}()
	}

	otoCtx, err := oto.NewContext(16000, 1, 2, 1024)
	if err != nil {
		panic(err)
	}
	player := otoCtx.NewPlayer()
	defer player.Close()

	for data := range ch {
		locker.Lock()
		locker.Unlock()

		if statSkip && data.Pos > 1 {
			continue
		}

		if data.Pos == 1 {
			fullName = bakFullName
			atomic.StoreInt32(&playPosLog, 0)
			statSkip = false
		}

		fmt.Printf("%d>%s\n", data.Pos, data.Text)
		player.Write(data.Pcm)
		atomic.StoreInt32(&playPosLog, data.Pos)
		//fmt.Println("play:", playPosLog)
	}

	fmt.Println("End playback")
}

var c chan os.Signal = make(chan os.Signal, 1)

func wait_sig() {
	signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGABRT, syscall.SIGQUIT)
	<-c
	posLog := atomic.LoadInt32(&playPosLog)
	fmt.Printf("中途退出\n阅读记录: \n%s\n%d\n", fullName, posLog)
	logFileName(posLog)
	os.Exit(0)
}

func ttsInit2(v int) bool {
	data, err := ioutil.ReadFile("login-param.txt")
	if err != nil {
		return false
	}
	data = bytes.TrimSpace(data)

	if C.tts_init3(C.CBytes(data), C.int(v)) != 0 {
		return false
	}
	//fmt.Println("ttsInit2 successful")
	return true
}
